Skip to main content

Odoo PSA Architecture

Objective: Define the architecture for a custom Asset Management module in Odoo 19 (Studio) that acts as the central hub for IT operations.

Core Concepts

  • Asset Centricity: Every ticket, subscription line item, and security alert revolves around the x_client_asset record.
  • Bi-Directional Sync: Odoo is the Master for Clients (Partners), Pulseway is the Master for Device Details.
  • No-Code Constraint: Logic must be implemented using Odoo Studio Automated Actions (Python Server Actions) without custom backend modules.

Data Model: Client Asset (x_client_asset)

Field NameTypeDescription
x_nameCharComputer Hostname (Primary Key)
x_serial_numberCharBIOS Serial Number
x_partner_idMany2oneLink to res.partner (Company)
x_assigned_user_idMany2oneLink to res.partner (Contact/Person)
x_pulseway_idCharUnique System Identifier from Pulseway
x_os_infoCharOperating System Version
x_last_auditDatetimeLast sync timestamp
x_subscription_activeBooleanComputed field for billing

Odoo Studio Implementation Steps (T008, T013)

  1. Create Model: Open Studio -> New App "PSA" -> New Model "Client Asset" (x_client_asset).
  2. Add Fields: Drag and drop fields corresponding to the Data Model above.
    • Set x_name as the "Rec Name".
    • Set x_pulseway_id as indexed (for faster lookups during sync).
  3. Views:
    • List View: Show Name, Partner, OS, Last Audit.
    • Form View: Group details, add "Helpdesk Tickets" smart button (One2many relation).
  4. Helpdesk Integration:
    • Open "Helpdesk" App -> Open Ticket Form -> Studio.
    • Add Many2one field x_asset_id pointing to x_client_asset.
    • Add domain filter to x_asset_id: [('x_partner_id', '=', partner_id)] (Only show assets belonging to the ticket's customer).

Server Action Logic (T010, T011, T015)

1. Webhook Handler: System Registered & Alerts (Python)

Trigger: Incoming Webhook (URL called by Pulseway/RocketCyber)

# T010 & T011 Combined Logic
payload = request.jsonrequest
event_type = payload.get('EventType') # Pulseway Header or Payload field

if event_type == 'SystemRegistered':
# --- Sync Asset Logic ---
sys_id = payload.get('SystemIdentifier')
asset = env['x_client_asset'].search([('x_pulseway_id', '=', sys_id)], limit=1)

vals = {
'x_name': payload.get('Name'),
'x_os_info': payload.get('OS'),
'x_serial_number': payload.get('CustomFields', {}).get('Serial'),
'x_last_audit': datetime.now(),
}

if asset:
asset.write(vals)
else:
# Link to Partner based on Group Name
group_name = payload.get('Group')
partner = env['res.partner'].search([('name', '=', group_name)], limit=1)
if not partner:
# Fallback: Log warning or create dummy partner?
# For now: Do not create asset if partner not found to ensure data integrity
log("Partner not found for group: " + group_name)
else:
vals['x_pulseway_id'] = sys_id
vals['x_partner_id'] = partner.id
env['x_client_asset'].create(vals)

elif event_type == 'Notification':
# --- Alert to Ticket Logic ---
sys_id = payload.get('SystemIdentifier')
asset = env['x_client_asset'].search([('x_pulseway_id', '=', sys_id)], limit=1)

if asset:
ticket_vals = {
'name': f"Alert: {payload.get('Header')}",
'description': payload.get('Message'),
'partner_id': asset.x_partner_id.id,
'x_asset_id': asset.id,
'priority': '2' if 'Critical' in payload.get('Priority', '') else '1',
'team_id': env.ref('helpdesk.support_team').id
}
env['helpdesk.ticket'].create(ticket_vals)

2. Downstream Sync: Create Client in Pulseway (T012)

Trigger: On Creation of res.partner (Automated Action)

# T012 Logic
if record.is_company: # Only for parent companies
import requests
import json

url = "https://api.pulseway.com/v2/organizations"
headers = {
"Authorization": "Bearer " + env['ir.config_parameter'].get_param('pulseway.api_key'),
"Content-Type": "application/json"
}
data = {
"Name": record.name,
"Description": "Synced from Odoo"
}

try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
except Exception as e:
# Log error to Chatter
record.message_post(body=f"Failed to sync to Pulseway: {str(e)}")

3. Billing Sync: Update Subscriptions (T015, T016)

Trigger: Scheduled Action (e.g., Monthly on the 15th)

# T015 Logic
managed_seat_product_id = env.ref('product.managed_seat_product').id # Replace with actual XML ID or search
subscriptions = env['sale.order'].search([
('state', '=', 'sale'),
('is_subscription', '=', True)
])

for sub in subscriptions:
partner = sub.partner_id
# Count active assets
asset_count = env['x_client_asset'].search_count([
('x_partner_id', '=', partner.id),
('x_status', '=', 'active') # T016: Handle decommissioned assets
])

# Update or Create Line Item
line = sub.order_line.filtered(lambda l: l.product_id.id == managed_seat_product_id)
if line:
line.write({'product_uom_qty': asset_count})
sub.message_post(body=f"Auto-updated Asset count to {asset_count}")
elif asset_count > 0:
# Create line if missing but assets exist
env['sale.order.line'].create({
'order_id': sub.id,
'product_id': managed_seat_product_id,
'product_uom_qty': asset_count
})